/**
 * File:   DefaultNode.java
 * Author: Gvennet
 */
package dht.emulator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/** The default implementation of the <code>Node</code> interface. */
public final class DefaultNode implements Node
{

    private static final int NODE_LIST_SIZE = 4;

    private static final int ROUTING_TABLE_SIZE = 4;


    private final Identifier identifier;

    private final Map<Identifier, String> values;

    private final List<Node> nodes;


    /** Creates a new node with random identifier and empty routing table. */
    public DefaultNode()
    {
        this(Identifier.randomIdentifier(20), null);
    }

    /**
     * Creates a new node with random identifier and the one node in the routing table.
     *
     * @param knownNode the node to add in the routing table.
     */
    public DefaultNode(Node knownNode)
    {
        this(Identifier.randomIdentifier(20), knownNode == null ? null : Collections.singletonList(knownNode));
    }

    /**
     * Creates a new node with specified identifier and routing table containing specified nodes.
     *
     * @param identifier - the node identifier.
     * @param knownNodes - nodes to add to the routing table.
     *
     * @throws IllegalArgumentException if <code>identifier</code> is <code>null</code>
     */
    public DefaultNode(Identifier identifier, List<Node> knownNodes)
    {
        if (identifier == null)
        {
            throw new IllegalArgumentException("identifier is null");
        }
        this.identifier = identifier;
        values = new HashMap<Identifier, String>();
        nodes = new ArrayList<Node>();
        if (knownNodes != null)
        {
            if (knownNodes.contains(null))
            {
                throw new IllegalArgumentException("knownNodes contains null");
            }
            nodes.addAll(knownNodes);
        }
    }


    @Override
    public Identifier getIdentifier()
    {
        return identifier;
    }


    @Override
    public void init()
    {
        Set<Identifier> knownNodeIdentifiers = new HashSet<Identifier>();
        SortedSet<Node> result = new TreeSet<Node>(new NodeComparatorByDistance(identifier));
        knownNodeIdentifiers.add(identifier);
        for (Node node : nodes)
        {
            knownNodeIdentifiers.add(node.getIdentifier());
            result.add(node);
        }
        Queue<Node> queue = new LinkedList<Node>(nodes);
        while (!queue.isEmpty())
        {
            Node node = queue.remove();
            List<Node> selfSearch = node.getNodes(identifier, this);
            for (Node n : selfSearch)
            {
                if (!knownNodeIdentifiers.contains(n.getIdentifier()))
                {
                    knownNodeIdentifiers.add(n.getIdentifier());
                    result.add(n);
                    if (result.size() > ROUTING_TABLE_SIZE)
                    {
                        Node removed = result.last();
                        result.remove(removed);
                        if (!n.getIdentifier().equals(removed.getIdentifier())) {
                            queue.add(n);
                        }
                    }
                    else
                    {
                        queue.add(n);
                    }
                }
            }
        }
        nodes.clear();
        while (result.size() > ROUTING_TABLE_SIZE)
        {
            result.remove(result.last());
        }
        nodes.addAll(result);
    }


    @Override
    public int getQuality()
    {
        return Integer.MAX_VALUE;
    }

    @Override
    public int getQuality(Node sender)
    {
        if (sender == null)
        {
            throw new IllegalArgumentException("sender is null");
        }
        updateNodes(sender);
        return getQuality();
    }

    @Override
    public List<Node> getNodes()
    {
        return Collections.unmodifiableList(nodes);
    }

    @Override
    public List<Node> getNodes(Identifier id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("identifier is null");
        }
        List<Node> list = new ArrayList<Node>(nodes);
        Collections.sort(list, new NodeComparatorByDistance(id));
        return list.size() > NODE_LIST_SIZE ? list.subList(0, NODE_LIST_SIZE) : list;
    }

    @Override
    public List<Node> getNodes(Identifier id, Node sender)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("identifier is null");
        }
        if (sender == null)
        {
            throw new IllegalArgumentException("sender is null");
        }
        updateNodes(sender);
        return getNodes(id);
    }


    @Override
    public String getValue(Identifier key)
    {
        if (key == null)
        {
            throw new IllegalArgumentException("identifier is null");
        }
        return values.get(key);
    }

    @Override
    public String getValue(Identifier key, Node sender)
    {
        if (key == null)
        {
            throw new IllegalArgumentException("identifier is null");
        }
        if (sender == null)
        {
            throw new IllegalArgumentException("sender is null");
        }
        updateNodes(sender);
        return getValue(key);
    }

    @Override
    public void putValue(Identifier key, String value)
    {
        if (key == null)
        {
            throw new IllegalArgumentException("identifier is null");
        }
        if (value == null)
        {
            throw new IllegalArgumentException("value is null");
        }
        values.put(key, value);
    }

    @Override
    public void putValue(Identifier key, String value, Node sender)
    {
        if (key == null)
        {
            throw new IllegalArgumentException("identifier is null");
        }
        if (value == null)
        {
            throw new IllegalArgumentException("value is null");
        }
        if (sender == null)
        {
            throw new IllegalArgumentException("sender is null");
        }
        updateNodes(sender);
        putValue(key, value);
    }

    private void updateNodes(Node sender)
    {
        if (sender == null)
        {
            throw new IllegalArgumentException("sender is null");
        }
        Identifier id = sender.getIdentifier();
        if (!identifier.equals(id))
        {
            if (nodes.contains(sender))
            {
                nodes.remove(sender);
                nodes.add(0, sender);
            }
            else
            {
                if (nodes.size() < ROUTING_TABLE_SIZE)
                {
                    nodes.add(sender);
                }
                else
                {
                    List<Node> list = new ArrayList<Node>(nodes);
                    Collections.sort(list, new NodeComparatorByQuality());
                    if (sender.getQuality() > list.get(list.size() - 1).getQuality())
                    {
                        nodes.remove(list.get(list.size() - 1));
                        nodes.add(sender);
                    }
                }
                Iterator<Map.Entry<Identifier, String>> iterator = values.entrySet().iterator();
                while (iterator.hasNext())
                {
                    Map.Entry<Identifier, String> entry = iterator.next();
                    Identifier key = entry.getKey();
                    if (sender.getIdentifier().getDistance(key).compareTo(identifier.getDistance(key)) < 0)
                    {
                        sender.putValue(key, entry.getValue());
                        iterator.remove();
                    }
                }
            }
        }
    }


    @Override
    public String toString()
    {
        StringBuilder result = new StringBuilder();
        result.append("Node[").append(identifier).append(",nodes={");
        boolean comma = false;
        for (Node node : nodes)
        {
            if (comma)
            {
                result.append(',');
            }
            result.append(node.getIdentifier());
            comma = true;
        }
        result.append("},values=").append(values).append(']');
        return result.toString();
    }

}
